"""
Copyright (C) 2015 Mattias Ugelvik
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
import os, sys, io, re, shutil, contextlib
from aka import rename, copy
from contex import rules
import nose

TEST_DIR = os.path.join(os.path.dirname(__file__), "TEST_DIR")


def make_dir():
    os.mkdir(TEST_DIR)

def clear_files():
    shutil.rmtree(TEST_DIR)




@nose.with_setup(make_dir, clear_files)
def test_cycle():
    with Dir("circle",
             File("File0"),
             File("File1"),
             File("File2")
    ) as files:
        assert True == rename(rules(r'File(\d)', {
            1: lambda i: (int(i) + 1) % 3
        }), prompt=False, location=files.fullname)
        files.exists()
        assert files[0].read() == 'File2'
        assert files[1].read() == 'File0'
        assert files[2].read() == 'File1'
    
@nose.with_setup(make_dir, clear_files)
def test_copy_fail():
    with Dir("copy",
             File("File0"),
             File("File1"),
             File("File2")
    ) as files:
        assert False == copy(rules(r'File(\d)', {
            1: lambda i: int(i) + 1
        }), prompt=False, location=files.fullname)
        files.exists(cmp_content=True)

@nose.with_setup(make_dir, clear_files)
def test_copy():
    start = Dir("copy",
                File("File0"),
                File("File1"),
                File("File2")).build(TEST_DIR)
    
    
    assert True == copy(rules(r'File(\d)', {
        1: lambda i: int(i) + 3
    }), prompt=False, location=start.fullname)
    
    result = Dir("copy",
                 File("File0"),
                 File("File1"),
                 File("File2"),
                 File("File3", "File0"),
                 File("File4", "File1"),
                 File("File5", "File2"))
    result.exists(cmp_content=True, parent=TEST_DIR)
    result.kill(TEST_DIR)
    
@nose.with_setup(make_dir, clear_files)
def test_collide():
    with Dir("collide",
             File("File0"),
             File("File1")
    ) as files:
        assert False == rename(lambda fn: "File",
                               prompt=False, location=files.fullname, emergency="rollback")
        files.exists(cmp_content=True)

@nose.with_setup(make_dir, clear_files)
def test_collide_different_directory():
    with Dir("base",
             Dir("collide",
                 File("File1")),
             Dir("start",
                 File("File0"),
                 File("File1"))
    ) as files:
        assert False == rename(lambda fn: '../collide/'+fn, prompt=False, location=files[1].fullname)
        files.exists(cmp_content=True)


@nose.with_setup(make_dir, clear_files)
def test_nocollide_different_directory():
    start = Dir("base",
                Dir("collide",
                    File("File1")),
                Dir("start",
                    File("File0"),
                    File("File1"))).build(TEST_DIR)
    
    assert True == rename(lambda fn: '../collide/'+fn if fn != 'File1' else False,
                          prompt=False, location=start[1].fullname)
    
    result = Dir("base",
                 Dir("collide",
                     File("File0"),
                     File("File1")),
                 Dir("start",
                     File("File1")))
    result.exists(parent=TEST_DIR)
    result.kill(TEST_DIR)

@nose.with_setup(make_dir, clear_files)
def test_empty():
    with Dir("collide") as files:
        assert True == rename(lambda fn: "File", location=files.fullname)
        files.exists()
        
@nose.with_setup(make_dir, clear_files)
def test_split_files():
    start = Dir("base",
                Dir("odd"),
                Dir("even"),
                Dir("start",
                    File("File0"),
                    File("File1"),
                    File("File2"),
                    File("File3"),
                    File("File4"))).build(TEST_DIR)

    def split(fn):
        even = int(re.search('\d+$', fn).group(0)) % 2 == 0
        return '../{}/{}'.format('even' if even else 'odd', fn)

    assert True == rename(split, prompt=False, location=start[2].fullname)
    
    result = Dir("base",
                 Dir("odd",
                     File("File1"),
                     File("File3")),
                 Dir("even",
                     File("File0"),
                     File("File2"),
                     File("File4")),
                 Dir("start"))
    result.exists(parent=TEST_DIR)
    result.kill(TEST_DIR)

@nose.with_setup(make_dir, clear_files)
def test_move_target_dir():
    with Dir("base",
             Dir("target"),
             Dir("start",
                 File("File3"),
                 File("File4"))) as files:
        assert False == rename(lambda fn: 'target/'+fn, prompt=False, location=files.fullname)


@nose.with_setup(make_dir, clear_files)
def test_move_target_dir_deeper():
    with Dir("base",
             Dir("target",
                 Dir("subtarget")),
             Dir("start",
                 File("File3"),
                 File("File4"))) as files:
        assert False == rename(lambda fn: 'target/subtarget/'+fn, prompt=False, location=files.fullname)


@nose.with_setup(make_dir, clear_files)
def test_move_dir():
    files = Dir("move_dir",
                Dir("target"),
                Dir("start",
                    File("File3"),
                    File("File4"))).build(TEST_DIR)
    
    assert True == rename(lambda fn: 'target/'+fn if fn == 'start' else False,
                          prompt=False, location=files.fullname)

    result = Dir("move_dir",
                 Dir("target",
                     Dir("start",
                         File("File3"),
                         File("File4"))))
    result.exists(parent=TEST_DIR)
    result.kill(TEST_DIR)
                         

@nose.with_setup(make_dir, clear_files)
def test_rollback():
    files = Dir("base",
             Dir("collide"),
             Dir("start",
                 File("File0"),
                 File("File1"))).build(TEST_DIR)
    
    error_file = os.path.join(TEST_DIR, 'base', 'collide', 'File1')

    def my_readline(*args, **kwargs):
        with open(error_file, 'w') as f:
            f.write("THIS IS MY PROPARTAY!")
            # ^ This file cannot have been detected cause the error checking has
            # already been done at this point
        inp.readline = old_readline
        return old_readline(*args, **kwargs)
        
    old_stdin = sys.stdin
    inp = io.StringIO('y\nshowroll\nrollback\n')
    old_readline = inp.readline
    inp.readline = my_readline
    sys.stdin = inp

    assert False == rename(lambda fn: '../collide/'+fn, location=files[1].fullname)
    
    sys.stdin = old_stdin
    assert inp.readline() == ''
    
    result = Dir("base",
                 Dir("collide",
                     File("File1", "THIS IS MY PROPARTAY!")),
                 Dir("start",
                     File("File0", "File0"),
                     File("File1", "File1")))
    result.exists(parent=TEST_DIR, cmp_content=True)
    result.kill(TEST_DIR)


@nose.with_setup(make_dir, clear_files)
def test_copy_rollback():
    files = Dir("base",
             Dir("collide"),
             Dir("start",
                 File("File0"),
                 File("File1"))).build(TEST_DIR)
    
    error_file = os.path.join(TEST_DIR, 'base', 'collide', 'File1')

    def my_readline(*args, **kwargs):
        with open(error_file, 'w') as f:
            f.write("THIS IS MY PROPARTAY!")
            # ^ This file cannot have been detected cause the error checking has
            # already been done at this point
        inp.readline = old_readline
        return old_readline(*args, **kwargs)
        
    old_stdin = sys.stdin
    inp = io.StringIO('y\nshowroll\nrollback\n')
    old_readline = inp.readline
    inp.readline = my_readline
    sys.stdin = inp

    assert False == copy(lambda fn: '../collide/'+fn, location=files[1].fullname)
    
    sys.stdin = old_stdin
    assert inp.readline() == ''
    
    result = Dir("base",
                 Dir("collide",
                     File("File1", "THIS IS MY PROPARTAY!")),
                 Dir("start",
                     File("File0", "File0"),
                     File("File1", "File1")))
    result.exists(parent=TEST_DIR, cmp_content=True)
    result.kill(TEST_DIR)



@nose.with_setup(make_dir, clear_files)
def test_updown():
    out = io.StringIO("")
    with Dir("updown",
             File("File0"),
             File("File1")
    ) as files, contextlib.redirect_stdout(out):
        assert True == rename(lambda fn: "../updown/" + fn,
                              prompt=False, location=files.fullname, emergency="rollback")

        assert "no files to rename" in out.getvalue().lower()
        files.exists(cmp_content=True)


####
###
##  A little API for creating files and stuff:
###
####


class FSEntry:
    def __init__(self, name, *content):
        self.name, self.content, self.fullname = name, content, None
    def __enter__(self):
        return self.build(TEST_DIR)
    def __exit__(self, *args):
        self.kill()
    def __getitem__(self, i):
        return self.content[i]
    def getpath(self, parent):
        return self.fullname if parent is None else os.path.join(parent, self.name)


class File(FSEntry):
    def __init__(self, name, *content):
        super().__init__(name, *content)
        self.content = self.name if not self.content else self.content[0]
        

    def build(self, parent_dir):
        self.fullname = os.path.join(parent_dir, self.name)
        with open(self.fullname, "w") as f:
            f.write(self.content)
        return self
    def kill(self, parent=None):
        os.remove(self.getpath(parent))
    def read(self, parent=None):
        with open(self.getpath(parent), "r") as f:
            return f.read()
        
        
    def exists(self, parent=None, cmp_content=False):
        path = self.getpath(parent)
        assert os.path.isfile(path)
        if cmp_content:
            assert self.read(parent) == self.content, (self.read(parent), self.content)


class Dir(FSEntry):
    def build(self, parent_dir):
        self.fullname = os.path.join(parent_dir, self.name)
        os.mkdir(self.fullname)
        for child in self.content:
            child.build(self.fullname)
        return self
    def kill(self, parent=None):
        " Recursively kill all the children, then commit suicide "
        path = self.getpath(parent)
        for child in self.content:
            child.kill(path)
        os.rmdir(path)
        
    def exists(self, parent=None, cmp_content=False):
        """ Recursively check if the filesystem actually looks like this tree-structure.
        If `self.build` is not called then `parent` must be provided """
        path = self.getpath(parent)
        assert os.path.isdir(path), path
        assert len(os.listdir(path)) == len(self.content) # No extra files
        for child in self.content:
            child.exists(path, cmp_content)


@nose.with_setup(make_dir, clear_files)
def test_exists():
    with Dir("circle", File("File0"), File("File1"), File("File2")) as files:
        files.exists()

@nose.with_setup(make_dir, clear_files)
def test_exists_cmp_content():
    with Dir("circle", File("File0"), File("File1"), File("File2")) as files:
        files.exists(cmp_content=True)

